<?php

/**
 * С помощью этого класса выбираются id объектов, заданных типов, удволетворяющих условиям фильтрации
 */
class Selection implements iSelection {

	private
		$aResult           = null,
		$totalCount        = null,
		$aTypes            = array (),
		$aTypesFlags       = array (),
		$aFilters          = array (),
		$aGuidesJoins      = array (),
		$aTagsJoins        = array (),
		$sLogic            = 'AND',
		$aLimit            = array (),
		$orderField        = array ('id', true),
		$orderRand         = false,
		$blockTypes        = false,
		$aJustFollowJoined = array (),
		$aFollowJoins      = array (),
		$aSelectedFields   = array ();
	
	public function __construct () {}
	
	/**
	 * Добавляет тип объектов
	 * @param Type $oType
	 * @return void
	 */
	public function addType (Type $oType) {
		if ($this->blockTypes) {
			return false;
		}
		
		$typeId = $oType->getId ();
		
		$this->aTypes[] = $oType;
		$this->aTypesFlags[$typeId] = array ('find_in_guides' => false, 'find_in_tags' => false, 'find_in_real' => false);
		$this->aFilters[$typeId] = array ();
	}
	
	public function addFollowFilter (Type $oType, array $aFollowConnections, $fieldName, $fieldValue, $operator = '=') {
		$typesController = TypesController::getInstance ();
		
		$this->blockTypes = true;
		
		$typeId = $oType->getId ();
		$fieldName  = (string) $fieldName;
		$fieldValue = (string) $fieldValue;
		
		if (!$oField = $oType->getFieldByName ($fieldName)) {
			throw new CoreException; // TODO
		}
		
		if (!$oField->isReal() || $oField->getCharacter() == 'guide' || $oField->getCharacter() == 'multiguide' || $oField->getCharacter() == 'tags') {
			throw new CoreException; // TODO
		}
		
		foreach ($this->aTypes as $oBaseType) {
			
			$baseTypeId = $oBaseType->getId();
			
			if (!isset ($aFollowConnections[$baseTypeId])) {
				throw new CoreException; // TODO
			}
			
			$oTypesConnection = $aFollowConnections[$baseTypeId];
			
			if (in_array ($baseTypeId . '_' . $typeId, $this->aJustFollowJoined)) {
				$aFilters[$baseTypeId][$fieldName] = "\r\n\t\t`" . $oType->getTableName() . "`.`" . $fieldName . "` " . $operator . " '" . safeSql($fieldValue) . "'";
			}
			
			if (!$oChildConectedField = FieldsController::getInstance()->getField ($oTypesConnection->getValue ('child_rel_field_id'))) {
				return;
			}
			
			$sParentFieldName = 'id';
			if ($oTypesConnection->getValue ('parent_field_id') != 0) {
				if (!$oParentConnectedField = FieldsController::getInstance()->getField ($oTypesConnection->getValue ('parent_field_id'))) {
					throw new CoreException; // TODO
				}
				$sParentFieldName = $oParentConnectedField->getName ();
			}
			
			if ($oTypesConnection->getValue ('parent_type_id') == $baseTypeId) {
				$this->aFollowJoins[$baseTypeId][] = "\r\nLEFT JOIN `"  . $oType->getTableName() . "` ON `" . $oBaseType->getTableName() . "`.`" . $sParentFieldName . "` = `"  . $oType->getTableName() . "`.`" . $oChildConectedField->getName() . "`";
			} else {
				$this->aFollowJoins[$baseTypeId][] = "\r\nLEFT JOIN `"  . $oType->getTableName() . "` ON `" . $oBaseType->getTableName() . "`.`" . $oChildConectedField->getName() . "` = `"  . $oType->getTableName() . "`.`" . $sParentFieldName . "`";
			}
			
			$this->aTypesFlags[$baseTypeId]['find_in_real'] = true;			
			$this->aFilters[$baseTypeId][$fieldName] = "\r\n\t`" . $oType->getTableName() . "`.`" . $fieldName . "` " . $operator . " '" . safeSql($fieldValue) . "'";
			
			$this->aJustFollowJoined[] = $baseTypeId . '_' . $typeId;
		}
	}
	
	/**
	 * Добавляет произвольное поле, значения которого также будут выбраны наряду с id
	 * @param string $propertyName
	 */
	public function addSelectedProperty ($propertyName) {
		if ($propertyName == 'id' || in_array ($propertyName, $this->aSelectedFields)) {
			return;
		}
		
		foreach ($this->aTypes as $oType) {
			if (!$oField = $oType->getFieldByName ($propertyName)) {
				throw new CoreException ('none selected field'); // TODO
			}
			
			if (!$oField->isReal () || $oField->getCharacter () == 'guide' || $oField->getCharacter () == 'multiguide' || $oField->getCharacter () == 'tags') {
				throw new CoreException ('unresolved characrer of slected field'); // TODO
			}
		}
		
		$this->aSelectedFields[] = $propertyName;
	}
	
	/**
	 * Добавляет фильтр по свойству
	 * @param string $fieldName имя поля
	 * @param mixed $fieldValue значение поля
	 * @param string $operator логический оператор 
	 * @return void
	 */
	public function addFilter ($fieldName, $fieldValue, $operator = '=') {
		$this->blockTypes = true;
		
		$fieldName  = (string) $fieldName;
		
		$this->addFieldToFilters ($fieldName, $fieldValue, $operator);
	}
	
	public function addFilterIn ($sFieldName, array $values) {
		$this->blockTypes = true;
		
		$this->addFieldToFilters ((string) $sFieldName, $values, '');
	}
	
	public function addFilterIsNULL ($fieldName) {
		$this->blockTypes = true;
		
		$this->addFieldToFilters ((string) $sFieldName, null);
	}
	
	/**
	 * Добавляет группу фильтров
	 * @param array $arrayFilters массив фильтров вида: имя поля, значение поля, логический оператор (по-умолчанию "=") 
 	 * @param string $logicOperetor оператор, соединяющий условия в группе
	 * @return void
	 */
	public function addFiltersGroup (array $arrayFilters, $logicOperator = 'AND') {
		$this->blockTypes = true;
		
		$aTempFiltersGuides = array ();
		$aTempFiltersTags = array ();
		$aTempFiltersTypical = array ();
		$aTempTypes = array ();
		
		$aFilters = array ();
		
		foreach ($this->aTypes as $oType) {
			$typeId = $oType->getId ();
			
			foreach ($arrayFilters as $filter) {
            	$fieldName   = (string) $filter[0];
            	$fieldValue  = (string) isset ($filter[1]) ? $filter[1] : '';
            	$operator    = (string) isset ($filter[2]) ? $filter[2] : '=';
			
            	if ($fieldName != 'id') {
					// Запрещаем фильтрацию, если у типа нет поля с таким именем, если поле виртуальное
					if (!$oField = $oType->getFieldByName ($fieldName)) {
						throw new ObjectException (lang ('error_objlist_filtered_field_not_exists',__CLASS__));
					}
					if (!$oField->isReal ()) {
						throw new ObjectException (lang ('error_objlist_filtered_field_not_exists',__CLASS__));
					}
					
					$character = $oField->getCharacter ();
					
					if ($character == 'guide' || $character == 'multiguide') {
						continue;
					}
					if ($character == 'tags') {
						continue;
					}
            	}
            	
            	$this->aTypesFlags[$typeId]['find_in_real'] = true;
            	$aFilters[$typeId][] = "\r\n\t\t`" . $oType->getTableName() . "`.`" . $fieldName . "` " . $operator . " '" . safeSql($fieldValue) . "'";
			}
		}
		
		foreach ($this->aTypes as $oType) {
			$typeId = $oType->getId ();
			
			$this->aFilters[$typeId][] = "\r\n\t(" . implode ("\r\n\t\t" . $logicOperator . ' ', $aFilters[$typeId]) . "\r\n\t)";
		}
	}
	
	/**
	 * Устанавливает логоческий оператор ИЛИ (OR)
	 * @return void
	 */
	public function setLogicOr () {
		$this->sLogic = 'OR';
	}
	
	/**
	 * Устанавливает логоческий оператор И (AND)
	 * @return void
	 */
	public function setLogicAnd () {
		$this->sLogic = 'AND';
	}
	
	/**
	 * Устанавливает поле, по которому производится сортировка
	 * @param string $fieldName
	 * @param boolean $asc порядок сортировки: true - прямой (по-умолчанию), false - обратный
	 * @return void
	 */
	public function setOrder ($fieldName, $asc = true) {
		if ($fieldName == 'id') {
			$this->orderField = array ('id', (bool) $asc);
			return;
		}
		
		if (in_array ($fieldName, $this->aSelectedFields)) {
			$this->orderField = array ($fieldName, (bool) $asc);
			return;
		}
		
		foreach ($this->aTypes as $oType) {
			$aTypeFields = $oType->getRealFieldsNames ();
			
			if (!in_array ($fieldName, $aTypeFields)) {
				throw new ObjectException (lang ('error_objlist_filtered_field_not_exists',__CLASS__));
			}
		}
		
		$this->orderField = array ($fieldName, (bool) $asc);
		$this->aSelectedFields[] = $fieldName;
	}
	
	public function setOrderRand () {
		$this->orderRand = true;
	}
	
	/**
	 * Устанавливает лимит
	 * @param integer $start
	 * @param integer $end
	 * @return void
	 */
	public function addLimit ($start, $end = false) {
		if ($end === false) {
			$end = (int) $start;
			$start = 0;
		} else {		
			$start = (int) $start;
			$end   = (int) $end;
		}
		
		$this->aLimit = array ($start, $end);
	}
	
	/**
	 * Возвращает массив идентификаторов объектов
	 * @return array
	 */
	public function getList ($print = false) {
		if ($this->aResult === null) {
			$this->load ($print);
		}
		return $this->aResult;
	}
	
	/**
	 * Возвращает общее число объектов, удовлетворяющих условиям фильтрации
	 * @return integer
	 */
	public function getTotalCount () {
		if ($this->totalCount !== null) {
			return $this->totalCount;
		}
		
		if (count ($this->aTypes) == 0) {
			throw new ObjectException (lang('error_types_not_added',__CLASS__));
		}
		
		$aTypesSelects = array ();
		$n = 0;
		
		foreach ($this->aTypes as $oType) {
			$typeId = $oType->getId ();
			
			$sTypeTableName = $oType->getTableName ();
			
			if (!isset ($this->aTypesFlags[$typeId])) {
				continue;
			}
			
			// Строим запрос к основной таблице типа, включающий в себя фильтрацию по тегам и/или справочникам
			
			$aCurrentFilters = array ();
			$sJoins = '';
			
			// Добавляем фильтрацию и джоины от справочников
			if ($this->aTypesFlags[$typeId]['find_in_guides']) {
				//$sJoins .= "\r\nLEFT JOIN `explay_guides_content` ON `" . $sTypeTableName . "`.`id` = `explay_guides_content`.`object_id` ";
				$sJoins .= "\r\n" . implode("\r\n", $this->aGuidesJoins[$typeId]);
			}
			
			// Добавляем фильтрацию и джоины от тегов
			if ($this->aTypesFlags[$typeId]['find_in_tags']) {
				//$sJoins .= "\r\nLEFT JOIN `explay_tags` ON `" . $sTypeTableName . "`.`id` = `explay_tags`.`object_id` ";
				$sJoins .= "\r\n" . implode("\r\n", $this->aTagsJoins[$typeId]);
			}
			
			// Джоины следящих фильтров
			if (isset ($this->aFollowJoins[$typeId])) {
				$sJoins .= implode ('', $this->aFollowJoins[$typeId]);
			}
			
			// Свои фильтры
			if (count ($this->aFilters[$typeId]) > 0) {
				$aCurrentFilters[] = implode ("\r\n\t" . $this->sLogic . ' ', $this->aFilters[$typeId]);
			}
			
			$sFilters = count ($aCurrentFilters) > 0 ? "\r\nWHERE " . implode ("\r\n\t" . $this->sLogic . ' ', $aCurrentFilters) : '';
			
			$aTypesSelects[] = "(SELECT COUNT(*), " . (++$n) . " AS '__fuckin_union__' FROM `" . $oType->getTableName() . "` " . $sJoins . " " . $sFilters . ")";
		}

		$sSql = implode ("\r\n\r\nUNION\r\n\r\n", $aTypesSelects);
		
		$db = DBController::getInstance ();
		
		$get = $db->query ($sSql);
		
		$this->totalCount = 0;
		
		while (list ($count,) = $db->fetch_row ($get)) {
			$this->totalCount += (int) $count;
		}
		
		return $this->totalCount;
	}
	
	/**
	 * Возвращает загруженных
	 * @return integer
	 */
	public function getLoadedCount () {
		if ($this->aResult === null) {
			$this->load ();
		}
		return count ($this->aResult);
	}
	
	/**
	 * Загружает id
	 * @return void
	 */
	private function load ($print = false) {
		if (count ($this->aTypes) == 0) {
			throw new ObjectException (lang('error_types_not_added',__CLASS__));
		}
		
		$aTypesSelects = array ();
		
		foreach ($this->aTypes as $oType) {
			$typeId = $oType->getId ();
			$sTypeTableName = $oType->getTableName ();
			
			// Задача следующего кода составить правильный запрос с фильтрацией по значениям справочников,
			// значениям тегов и основной таблице типа.
			
			$aCurrentFilters = array ();
			$sJoins = '';
			
			// Добавляем фильтрацию и джоины от справочников
			if ($this->aTypesFlags[$typeId]['find_in_guides']) {
				//$sJoins .= "\r\nLEFT JOIN `explay_guides_content` ON `" . $sTypeTableName . "`.`id` = `explay_guides_content`.`object_id` ";
				$sJoins .= "\r\n" . implode("\r\n", $this->aGuidesJoins[$typeId]);
			}
			
			// Добавляем фильтрацию и джоины от тегов
			if ($this->aTypesFlags[$typeId]['find_in_tags']) {
				//$sJoins .= "\r\nLEFT JOIN `explay_tags` ON `" . $sTypeTableName . "`.`id` = `explay_tags`.`object_id` ";
				$sJoins .= "\r\n" . implode("\r\n", $this->aTagsJoins[$typeId]);
			}
			
			// Джоины следящих фильтров
			if (isset ($this->aFollowJoins[$typeId])) {
				$sJoins .= implode ('', $this->aFollowJoins[$typeId]);
			}
			
			// Свои фильтры
			if (count ($this->aFilters[$typeId]) > 0) {
				$aCurrentFilters[] = implode ("\r\n\t" . $this->sLogic . ' ', $this->aFilters[$typeId]);
			}
			
			$sFilters = count ($aCurrentFilters) > 0 ? "\r\nWHERE " . implode ("\r\n\t" . $this->sLogic . ' ', $aCurrentFilters) : '';
			
			$sSelectedFields = count ($this->aSelectedFields) ? ', `' . implode('`, `', $this->aSelectedFields) . '`' : '';
			
			$aTypesSelects[] = "(SELECT DISTINCT `" . $oType->getTableName() . "`.`id`, '" . $typeId . "' AS `__type__` " . $sSelectedFields . " FROM `" . $oType->getTableName() . "` " . $sJoins . " " . $sFilters . "\r\n)";
		}

		$sUnion = implode ("\r\n\r\nUNION\r\n\r\n", $aTypesSelects);
		
		if ($this->orderRand) {
			$sSql = $sUnion . "\r\nORDER BY RAND()";
		} else {
			$sSql = $sUnion . "\r\nORDER BY `" . $this->orderField[0] . '`' . (!$this->orderField[1] ? ' DESC' : '');
		}
		
		if (count ($this->aLimit) > 1) {
			$sSql .= "\r\nLIMIT "  . $this->aLimit[0] . ', ' . $this->aLimit[1];
		}
		
		if ($print) exit ($sSql);
		
		// Тут могло бы быть кеширование, и оно здесь будет!
		
		$db = DBController::getInstance ();
		
		$get = $db->query ($sSql);
		
		$this->aResult = array ();
		
		while ($row = $db->fetch_array ($get)) {
			$result = array (
				0 => $row['id'],
				1 => $row['__type__']
			);
			
			foreach ($this->aSelectedFields as $fieldName) {
				$result[$fieldName] = $row[$fieldName];
			}
			
			$this->aResult[] = $result;
		}
	}
	
	/**
	 * Добавляет фильтр
	 * @param string $fieldName
	 * @param mixed $fieldValue
	 * @param string $operator
	 */
	private function addFieldToFilters ($fieldName, $fieldValue, $operator) {
		foreach ($this->aTypes as $oType) {
			
			$typeId = $oType->getId ();
			
			if ($fieldName != 'id') {
				// Запрещаем фильтрацию, если у типа нет поля с таким именем, если поле виртуальное
				if (!$oField = $oType->getFieldByName ($fieldName)) {
					throw new ObjectException (lang ('error_objlist_filtered_field_not_exists',__CLASS__));
				}
				if (!$oField->isReal ()) {
					throw new ObjectException (lang ('error_objlist_filtered_field_not_exists',__CLASS__));
				}
				
				$fieldId = $oField->getId();
				$character = $oField->getCharacter ();
				
				if ($character == 'guide' || $character == 'multiguide') {
					$this->aTypesFlags[$typeId]['find_in_guides'] = true;
					
					if (!isset($this->aGuidesJoins[$typeId][$fieldId])) {
						$this->aGuidesJoins[$typeId][$fieldId] = "LEFT JOIN `explay_guides_content` AS `guide_" . $fieldId ."` ON (`" . $oType->getTableName() . "`.`id` = `guide_" . $fieldId . "`.`object_id` AND `guide_" . $fieldId . "`.`field_id` = " . $fieldId . ")";
					}
					
					if (is_array ($fieldValue)) {
						$this->aFilters[$typeId][$fieldName] = "\r\n\t`guide_" . $fieldId ."`.`selected_id` IN ('" . implode("','", safeSql($fieldValue, true)) . "')";
					} else {
						$this->aFilters[$typeId][$fieldName] = "\r\n\t`guide_" . $fieldId ."`.`selected_id` = " . (int)$fieldValue;
					}
					continue;
				}
				if ($character == 'tags') {
					$this->aTypesFlags[$typeId]['find_in_tags'] = true;
					
					if (!isset($this->aTagsJoins[$typeId][$fieldId])) {
						$this->aTagsJoins[$typeId][$fieldId] = "LEFT JOIN `explay_tags` AS `tags_" . $fieldId ."` ON (`" . $oType->getTableName() . "`.`id` = `tags_" . $fieldId . "`.`object_id` AND `tags_" . $fieldId . "`.`field_id` = " . $fieldId . ")";
					}
					
					if (is_array ($fieldValue)) {
						$this->aFilters[$typeId][$fieldName] = "\r\n\t`tags_" . $fieldId . "`.`value` IN ('" . implode("','", safeSql($fieldValue, true)) . "')";
					} else {
						$this->aFilters[$typeId][$fieldName] = "\r\n\t`tags_" . $fieldId . "`.`value` = '" . safeSql($fieldValue, true) . "'";
					}
					continue;
				}
			}
			
			$this->aTypesFlags[$typeId]['find_in_real'] = true;
			if (is_array ($fieldValue)) {
				$this->aFilters[$typeId][$fieldName] = "\r\n\t`" . $oType->getTableName() . "`.`" . $fieldName . "` IN ('" . implode("','", safeSql($fieldValue, true)) . "')";
			} elseif ($fieldValue === null) {
				$this->aFilters[$typeId][$fieldName] = "\r\n\t`" . $oType->getTableName() . "`.`" . $fieldName . "` IS NULL";
			} else {
				$this->aFilters[$typeId][$fieldName] = "\r\n\t`" . $oType->getTableName() . "`.`" . $fieldName . "` " . $operator . " '" . safeSql($fieldValue, true) . "'";
			}
		}
	}
	
}